﻿using SW2A311_Lib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Threading;

namespace Server {
    /// <summary>
    /// Håndtere op til 100 forbindelser asynkront. Er afhængig af SW2A311_lib
    /// </summary>
    class Network {

        private const int SIZE = 4096;
        private byte[] _buffer = new byte[SIZE];
        private Socket _server;
        private Dictionary<Socket, User> _clientList = new Dictionary<Socket, User>();
        private static ManualResetEvent _allDone = new ManualResetEvent(false);
        private EventDrivenShare _EDS;
        private ObjectToByteArray _serializer = new ObjectToByteArray();
        private DataHandler _data;
        private CallBackNetwork sendCallBackMethod;
        private int projectIndex = 0; // får kæden StartSendProject-SendProject-EndSendProject til at virke rekursivt. Optimalt skulle det være en lokal variable, men CallBack metoder kan kun have et parameter

        /// <summary>
        /// Constructor. Initializere liste af <c>User</c> fra users.txt.
        /// </summary>
        /// <param name="EDSIn">EventDrivenShare som skal håndtere dataen</param>
        public Network(EventDrivenShare EDSIn) {
            _EDS = EDSIn;
            _data = new DataHandler("users.txt");
            sendCallBackMethod = new CallBackNetwork(SendData);
        }

        private delegate void CallBackNetwork(IAsyncResult iar); //bliver brugt til at styre hvilke callback metode der skal kaldes efter HandleData
        /// <summary>
        /// Start punkt for tråden
        /// </summary>
        public void Run() {

            try {
                _server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPEndPoint iep = new IPEndPoint(IPAddress.Any, 8100);
                IPHostEntry host;
                string serverIP = "";
                host = Dns.GetHostEntry(Dns.GetHostName());
                foreach (IPAddress ip in host.AddressList) {
                    if (ip.AddressFamily == AddressFamily.InterNetwork)
                        serverIP += ip.ToString() + ", ";
                }
                _server.Bind(iep);
                _EDS.ChatMessage = "Server initialized";
                _server.Listen(100);
                _EDS.ChatMessage = "Listening at: \"" + serverIP + "\" at a maximum of 100 connesctions at a time";

                while (true) {
                    _allDone.Reset();
                    _server.BeginAccept(new AsyncCallback(AcceptCon), _server); //starter et asynkron klad, der acceptere en forbindelse
                    _allDone.WaitOne();
                }
            }
            catch (SocketException e) {
                _EDS.ChatMessage = e.ToString();
            }
        }
        /// <summary>
        /// Callback metode til Socket.BeginAccept().
        /// </summary>
        /// <param name="iar">Forventer en socket som IAsyncResult</param>
        private void AcceptCon(IAsyncResult iar) {
            _allDone.Set();
            try {
                Socket oldServer = (Socket)iar.AsyncState;
                Socket client = oldServer.EndAccept(iar);
                client.NoDelay = true;
                string broadCastJoin = client.LocalEndPoint.ToString() + " has joined";
                _EDS.ChatMessage = client.RemoteEndPoint.ToString() + " connected";


                client.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveData), client);
            }
            catch (Exception e) {
                _EDS.ChatMessage = e.ToString();
                return;
            }
        }

        /// <summary>
        /// Callback metode til BeginSend().
        /// </summary>
        /// <param name="iar">Forventer en socket som IAsyncResult</param>
        void SendData(IAsyncResult iar) {
            Socket client = (Socket)iar.AsyncState;
            try {
                int sent = client.EndSend(iar);
                if (CheckIfSocketIsConnected(client)) //fordi uforventet disconnects er mulige skal der tjekkes om client er forbundet hver gang man prøver og modtage/sende
                    client.BeginReceive(_buffer, 0, SIZE, SocketFlags.None, new AsyncCallback(ReceiveData), client);
            }
            catch (SocketException e) {
                _clientList.Remove(client);
                _EDS.ChatMessage = "Error receiving: " + Convert.ToString(e.ErrorCode);
                return;
            }
        }

        /// <summary>
        /// Callback metode til BeginReceive().
        /// </summary>
        /// <param name="iar">Forventer en socket som IAsyncResult</param>
        void ReceiveData(IAsyncResult iar) {
            Socket client = (Socket)iar.AsyncState;
            try {
                int recv = client.EndReceive(iar);
                if (recv == 0) {
                    client.Close();
                    _server.BeginAccept(new AsyncCallback(AcceptCon), _server);
                    return;
                }
                byte[] reply = null;
                try {
                    reply = HandleData(_buffer, client);
                }
                catch (SerializationException) {

                }
                finally {
                    if (reply == null)
                        reply = _serializer.SerializeObject(new EncapsulationClass(new ChatMessageEncapsulation(""))); //array der sendes kan ikke være null og vi kan ikke stoppe sende/modtage loopet, så der kommes en 'tom' værdi..
                }
                if (CheckIfSocketIsConnected(client) && reply != null)
                    client.BeginSend(reply, 0, reply.Length, SocketFlags.None, new AsyncCallback(sendCallBackMethod), client);
            }
            catch (SocketException e) {
                _clientList.Remove(client);
                _EDS.ChatMessage = "Error sending: " + Convert.ToString(e.ErrorCode);
                return;
            }
        }

        /// <summary>
        /// Metode der håndtere data modtaget fra netværket.
        /// </summary>
        /// <param name="byteArray">Data der skal behandles. Skal være indkapslet i EncapsulationClass ellers kastes en SerializationException</param>
        /// <param name="currClient">Det socket der modtog dataen, bruges hvis dataen skal broadcastes</param>
        /// <returns>byte array </returns>
        private byte[] HandleData(byte[] byteArray, Socket currClient) {
            EncapsulationClass data;
            byte[] toReturn = null;
            data = _serializer.DeSerializeObject<EncapsulationClass>(byteArray); //deserializes byteArray til encapsulationClass



            switch (data.TypeCode) {
                case ObjectType.Project:
                    ProjectData project = _serializer.DeSerializeObject<ProjectData>(data.SentObject);
                    Broadcast(byteArray, currClient);
                    ProjectCollection.Instance.Add(project);
                    ProjectCollection.Instance.WriteDatabaseToFile(project.ProjectName, project);
                    _EDS.ChatMessage = "Added project, with admin: " + project.AssignedAdmin;
                    break;
                case ObjectType.Chat:
                    ChatMessageEncapsulation chatMsg = _serializer.DeSerializeObject<ChatMessageEncapsulation>(data.SentObject);
                    if (!chatMsg.UserName.Equals("")) { //da brugeren sender en tom ChatMessageEncapsulation som svar på at modtage et User- og ProjectData-Objekt skal den sorteres fra
                        _EDS.ChatMessage = chatMsg.UserName + ": " + chatMsg.ChatMessageContent;
                        Broadcast(byteArray, currClient);
                    }
                    break;
                case ObjectType.LogInUser:
                    User user = _serializer.DeSerializeObject<User>(data.SentObject);
                    User userToReturn = authenticateUser(user, currClient);
                    toReturn = _serializer.SerializeObject<EncapsulationClass>(new EncapsulationClass(userToReturn));
                    if (userToReturn.isValid)
                        sendCallBackMethod = StartSendProjectData;
                    _EDS.ChatMessage = "Handled user login try";
                    projectIndex = 0;
                    break;
                case ObjectType.CreateUser:
                    User userToCreate = _serializer.DeSerializeObject<User>(data.SentObject);
                    _data.Users.Add(new User(userToCreate.Username, userToCreate.Password));
                    _data.WriteUserDatabaseToFile();
                    _EDS.ChatMessage = "Added user";
                    break;
                default: break;
            }
            return toReturn;
        }

        /// <summary>
        /// Start af kæden der sender alle projekter til klient.
        /// </summary>
        /// <param name="iar">Forventer en socket som IAsyncResult</param>
        private void StartSendProjectData(IAsyncResult iar) {
            Socket client = (Socket)iar.AsyncState;
            int sent = client.EndSend(iar);
            client.BeginReceive(_buffer, 0, SIZE, SocketFlags.None, new AsyncCallback(SendProjectData), client);
        }
        /// <summary>
        /// Del af kæden der sender alle projekter til klienten.
        /// </summary>
        /// <param name="iar"></param>
        private void SendProjectData(IAsyncResult iar) {
            try {
                Socket client = (Socket)iar.AsyncState;
                int recv = client.EndReceive(iar);
                ProjectData pd;

                if (ProjectCollection.Instance.TryItemAtIndex(projectIndex, out pd)) {
                    byte[] projectToSend = _serializer.SerializeObject<EncapsulationClass>(new EncapsulationClass(pd));
                    _EDS.ChatMessage = "Project sent: " + pd.ProjectName;

                    if (CheckIfSocketIsConnected(client))
                        client.BeginSend(projectToSend, 0, projectToSend.Length, SocketFlags.None, new AsyncCallback(EndSendProjectData), client);
                }
                else {
                    sendCallBackMethod = SendData; //hvis projektet ikke findes, så konstatere vi at alle projekter er blever sendt (altså at projectIndex > antallet af projekter i ProjectCollection)
                    client.BeginReceive(_buffer, 0, SIZE, SocketFlags.None, new AsyncCallback(ReceiveData), client);
                }
                _allDone.Reset();
            }
            catch (SocketException e) {
                _EDS.ChatMessage = "Error receiving: " + Convert.ToString(e.ErrorCode);
                return;
            }
        }

        /// <summary>
        /// Del af kæden der sender alle projekter til klienten
        /// </summary>
        /// <param name="iar">Forventer en socket som IAsyncResult</param>
        void EndSendProjectData(IAsyncResult iar) {
            try {
                Socket client = (Socket)iar.AsyncState;
                int sent = client.EndSend(iar);

                projectIndex++;
                if (CheckIfSocketIsConnected(client))
                    client.BeginReceive(_buffer, 0, SIZE, SocketFlags.None, new AsyncCallback(SendProjectData), client);

            }
            catch (SocketException e) {
                _EDS.ChatMessage = "Error receiving: " + Convert.ToString(e.ErrorCode);
                return;
            }
        }

        /// <summary>
        /// Sender msgToBroadcast til alle forbundne klienter.
        /// </summary>
        /// <param name="msgToBroadcast">byte array der skal broadcastes.</param>
        /// <param name="toSkip">Socket der ikke skal sendes til. Hvis der skal sendes til alle sockets, angiv værdien som null</param>
        private void Broadcast(Byte[] msgToBroadcast, Socket toSkip) {
            foreach (Socket currClient in _clientList.Keys) {
                if (currClient != toSkip) {
                    Socket client = currClient; //skal erklæres uden for try/catch da vi skal bruge den i catch
                    try {
                        if (client.Connected)
                            client.Send(msgToBroadcast, 0, msgToBroadcast.Length, SocketFlags.None);
                    }
                    catch (SocketException e) {
                        _EDS.ChatMessage = "Error broadcasting: " + Convert.ToString(e.ErrorCode);
                        return;
                    }
                }
            }
        }

        /// <summary>
        /// Verificere en User udfra dets password og bruger navn. Sammen lignes med listen af User i klassens DataHandler
        /// </summary>
        /// <param name="userIn">User der skal tjekkes på</param>
        /// <param name="currSocket">socket User er forbundet via</param>
        /// <returns>returnere et User object der har dets <c>isValid</c> property sat afhængigt af om brugeren var gyldig</returns>
        private User authenticateUser(User userIn, Socket currSocket) {
            foreach (User u in _data.Users) {
                if (u.Username.Equals(userIn.Username))
                    if (u.Password.Equals(userIn.Password))
                        if (!CheckIfUsernameIsConnected(userIn)) { //tjekker om brugeren allerade er forbundet
                            ChatMessageEncapsulation connectedMsg = new ChatMessageEncapsulation("[Server]");
                            connectedMsg.ChatMessageContent = u.Username + " has connected!";
                            EncapsulationClass wrappedMsg = new EncapsulationClass(connectedMsg);
                            Broadcast(_serializer.SerializeObject<EncapsulationClass>(wrappedMsg), currSocket);
                            _clientList.Add(currSocket, userIn);
                            return new User(userIn.Username, userIn.Password, true);
                        }
            }
            return new User(userIn.Username, userIn.Password, false);

        }

        /// <summary>
        /// Tjekker om User er forbundet
        /// </summary>
        /// <param name="user">User der skal tjekkes om er forbundet</param>
        /// <returns>true; hvis User er forundet, ellers false</returns>
        private bool CheckIfUsernameIsConnected(User user) {
            for (int i = 0; i < _clientList.Count; i++) {
                KeyValuePair<Socket, User> u = _clientList.ElementAt<KeyValuePair<Socket, User>>(i);
                if (u.Value.Username.Equals(user.Username))
                    if (u.Key.Connected)
                        return true;
                    else
                        _clientList.Remove(u.Key);
            }
            return false;
        }

        /// <summary>
        /// Tjekker om et socket er forbundet
        /// </summary>
        /// <param name="currSocket">Socket der skal tjekkes om er forbundet</param>
        /// <returns>True; hvis socket er forbundet, ellers false</returns>
        private bool CheckIfSocketIsConnected(Socket currSocket) {
            if (currSocket.Connected)
                return true;
            else {
                _clientList.Remove(currSocket);
                return false;
            }
        }
    }
}
